Linguaggio C

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

Aggregazione dati: strutture

di Luca Sabatucci

Lezione 9

Pagina principale | Lezione precedente | Lezione successiva


Entità complesse: struct, union e enum

I tipi di dato che abbiamo visto finora sono intrinseci al compilatore: sono, cioè, dei tipi di dato che il compilatore è in grado di gestire senza ulteriori costruzioni logiche da parte del programmatore. Per questa ragione vengono chiamati "tipi elementari".
Questi, però, spesso non sono sufficienti a rappresentare in modo esauriente un gruppo di dati diversi collegati tra loro, ad esempio i vari dati disomogenei che compongono un indirizzo: l'array di caratteri che contiene il nome, quello che contiene il cognome, un longint che contiene il numero di telefono ecc.

Il linguaggio C mette a disposizione del programmatore alcuni strumenti per rappresentare entità complesse dando la possibilità di definire veri e propri tipi di dato "nuovi" (o quasi) e di gestirli come se fossero intrinseci al compilatore.

Le strutture

Tra gli strumenti cui si ho fatto cenno prima, il più importante è la struttura , mediante la quale si definisce un modello che individua un'aggregazione di tipi di dato fondamentali.

La parola chiave per la dichiarazione di una struttura è "struct" e gli elementi di una struttura possono essere di qualsiasi tipo di dati valido in C.

struct indirizzo { 
	char nome[30]; 
	char via[60]; 
	char citta[20]; 
	char stato[2]; 
	int cap; 
	long int telefono; 
};

Le cose da notare sono due: la prima è che all'interno della struttura ci possono essere tipi di dati diversi (array char, int, ecc.). La seconda è la sintassi: si mette un nome a piacere dopo "struct", poi si apre la parentesi { graffa, si immettono gli elementi terminati ognuno col punto e virgola, infine si chiude la parentesi graffa e si mette un punto e virgola finale.

Comunque questa dichiarazione (in questa forma: più avanti vedremo altre forme di dichiarazione che, invece, lo fanno) non comporta che il compilatore riservi dello spazio di memoria per l’allocazione dei campi della struttura stessa poiché essa non genera le variabili, ma la "forma" della struttura chiamata "indirizzo", cioè il suo modello.

Ossia definiamo quel "nuovo" tipo di dati complesso, astrattamente.

Per creare fisicamente una struttura del tipo descritto e poterlo utilizzare, occorre dichiararlo in maniera del tutto analoga a quanto avviene con le care vecchie variabili:

	struct indirizzo IndirizzoCliente; 

Soltanto ora abbiamo creato un esemplare della struttura indirizzo, che risponde al nome IndirizzoCliente pronta per essere usata con questo nome all’interno del programma.
All’atto dell’istanziazione della nostra struttura il compilatore assegnerà ad essa una quantità di memoria pari alla somma della quantità di memoria necessaria per i tipi elementari che essa contiene.

Di solito le dichiarazioni delle strutture compaiono all'inizio del sorgente, o comunque, in ogni caso, vanno dichiarate prima di essere utilizzate: solo dopo avere definito l'identificatore (può sembrare una ripetizione di quanto finora detto, ma è bene ribadirlo perché è un errore in cui molti incorrono) e il modello della struttura, come nell’esempio di poco fa, è possibile dichiarare ed utilizzare oggetti di quel tipo, vere e proprie variabili struct.

È possibile creare subito uno o più esemplari di strutture subito, al momento stesso della dichiarazione: è sufficiente mettere i nomi delle stesse tra la parentesi graffa } chiusa e il punto e virgola finale, separandoli con delle virgole:

struct indirizzo { 
	char nome[30]; 
	char via[60]; 
	char citta[20]; 
	char stato[2]; 
	int cap; 
	longint telefono; 
} IndirizzoCliente, IndirizzoAmico, IndirizzoFornitore;   

In questo modo, oltre a dichiarare la forma della struttura indirizzo, ne abbiamo creati subito 3 semplari: IndirizzoCliente, IndirizzoAmico, IndirizzoFornitore.

Se si sapesse a priori, invece, che nell’ambito del programma si userà solo una struttura di un determinato tipo, l'indentificatore della struttura (in questo caso "indirizzo") sarebbe superfluo:

struct { 
	char nome[30]; 
	char via[60]; 
	char citta[20]; 
	char stato[2]; 
	int cap; 
	long int telefono; 
} IndirizzoCliente;   

Abbiamo così omesso l'identificatore "indirizzo", creando solamente l'esemplare unico e irripetibile "IndirizzoCliente".

Ora sappiamo come si creano le strutture, vediamo come fare a leggere e scrivere nei suoi elementi...

Questo avviene tramite un operatore speciale del linguaggio C, l'operatore punto ".", che si mette tra il nome della struttura e il nome dell'elemento:

IndirizzoCliente.cap = 90124;   

Con questa istruzione scrivo nella variabile cap di IndirizzoCliente il valore 90124. Analogamente, con IndirizzoCliente.via e IndirizzoCliente.citta accedo ai campi via e citta nella struttura "IndirizzoCliente ", sia in lettura che in scrittura.

In altri termini, si dice che si è creato un riferimento a quel singolo elemento della struttura.

Questo riferimento, così creato, può essere passato come argomento ad una funzione, si possono fare operazioni su di esso e essere indirizzato in maniera autonoma dal resto della struttura.

Vediamo qualche esempio per capirci meglio.

Nell’ipotetico programma per il quale abbiamo già creato la struttura "indirizzo" e ne abbiamo creato un’istanza di nome IndirizzoCliente potremmo aver bisogno di operare all’interno dei singoli campi della struct.

Ad esempio possiamo visualizzare il contenuto del campo cap:

printf("Il cap e' %d", IndirizzoCliente.cap);   

Allo stesso modo, possiamo scrivere nel campo nome con gets():

gets(IndirizzoCliente.nome);

Si può persino accedere ai singoli elementi degli array all'interno della struttura:<

IndirizzoCliente.nome[0] = "P"; 
IndirizzoCliente.nome[1] = "i"; 
IndirizzoCliente.nome[2] = "p"; 
IndirizzoCliente.nome[3] = "p"; 
IndirizzoCliente.nome[4] = "o";   

Come si vede la notazione è la solita per gli array, ciò che cambia è quell’" IndirizzoCliente" iniziale. Vediamo ora un utilizzo semplice e compilabile di quanto detto fin qui.

#include <stdio.h>   

struct indirizzo {             /* Definiamo la struttura indirizzo*/ 
	char nome[30]; 
	char via[60]; 
	char citta[20]; 
}; 

main() { 
	struct indirizzo IndirizzoCliente;      /* Creiamo 1 esemplare della struttura 
											indirizzo di nome IndirizzoCliente */ 

	/* Inseriamo i dati */ 
	printf("Scrivi il tuo nome: "); 
	gets(IndirizzoCliente.nome);        	/* Input tramite gets */ 
	printf("Scrivi la via: "); 
	gets(IndirizzoCliente.via);   
	printf("Citta': "); 
	gets(IndirizzoCliente.citta);   

	/* E adesso stampiamoli sullo schermo */ 
	printf("\n\nTi chiami %s", IndirizzoCliente.nome); 
	printf("\nStai in via %s", IndirizzoCliente.via);   
	printf("Il tuo cap è %d", IndirizzoCliente.cap
	printf("\nE abiti nella città di %s", IndirizzoCliente.citta); 

	/* Ora stampiamo un carattere solamente di un array nella struttura */ 
	printf("\n\nE Il nome comincia per %c.\n", IndirizzoCliente.nome[0]); 
}

Naturalmente in un caso semplice come questo era più facile usare i 3 array e l’int "slegati" senza dover andare a scomodare le strutture.

Ma noi possiamo ancora spingerci oltre, componendo tra loro le singole strutture in modo da creare anche degli array di strutture o persino delle strutture di strutture.

Per quanto riguarda i primi il modo di procedere è piuttosto banale: per definire un array di strutture è necessario definire una struttura e poi dichiarare una array di quel tipo.

	struct indirizzo {       /*Definiamo una struttura "indirizzo"*/ 
	    char nome[30]; 
		char via[60]; 
	    char citta[20]; 
	};                      /*Senza crearne fisicamente ancora nessuna.*/ 

	struct indirizzo IndirizziAmici[200]; 

Così facendo abbiamo creato un array di 200 strutture di tipo "indirizzo", il cui nome e' IndirizziAmici, utilizzabile come una rubrica contenente al massimo 200 indirizzi.

L’accesso ad un determinato elemento di un particolare indirizzo all'interno di una rubrica così strutturata è possibile in questo modo:

printf("%s", IndirizziAmici[5].nome); 
printf("%s", IndirizziAmici[25].citta); 

Queste due istruzioni stampano rispettivamente il nome del sesto indirizzo e la città del ventiseiesimo (dal momento che, come sappiamo, il conteggio degli indici di un array parte da 0).

C’è da notare anche che, se due variabili struttura sono del medesimo tipo, è anche possibile assegnare l'una all'altra, ovvero copiare l'una nell'altra.

Le assegnazioni sono del tipo:

struttura1 = struttura2; 
IndirizziAmici[3] = IndirizziAmici[8];   

Come dicevamo all’inizio, gli elementi di una struttura possono essere di qualsiasi tipo di dati valido in C, compresi gli array e le strutture stesse. Quindi è possibile creare anche delle strutture nidificate, ossia delle strutture all'interno di altre strutture.

Vediamo come:

struct amici { 
   	struct indirizzo abitazione; 
    struct indirizzo lavoro; 
    int eta; 
    char PiattoPreferito[50]; 
};   

In questo caso notiamo che la struttura amici contiene tra i suoi elementi altre 2 strutture.

Per accedere a elementi "semplici"” come interi, float o array è sufficiente usare la basta usare la solita sintassi col punto a ci siamo ormai abituati:

Roberto.PiattoPreferito="Anatra all’arancia";   

Per accedere, invece, agli elementi nidificati della struttura indirizzo, occorre:

Roberto.abitazione.citta = "Palermo";   

Ossia, ci si riferisce agli elementi di ciascuna struttura a partire da sinistra verso destra e dall'esterno all'interno, usando come connettivo l'operatore punto ".".

L’unica limitazione imposta dalle strutture nidificate è che (ovviamente) una struttura non potrà mai contenere un’altra struttura avente il proprio stesso identificativo: per il compilatore sarebbe impossibile risolvere completamente la definizione della struttura, in quanto essa risulterebbe definita in funzione di se stessa.

Mettiamo in pratica quanto detto con un’altro esempio completo (e compilabile).

#include <stdio.h>     

struct indirizzo {           /* Definiamo una struttura "indirizzo" */ 
	char nome[30]; 
	char via[60]; 
	char citta[20]; 
};                

	struct Amici {                 /* Definiamo una struttura "Amici" */ 
	struct indirizzo abitazione; 
	char PiattoPreferito [60]; 
};   

main() {   
    int i; 
    struct Amici AmiciPresenti[4]; 	/* Array di 4 strutture Amici */   

    for(i=0; i<3; i++) {                       /* Con il for richiedo 3 
														indirizzi da riempire */ 
		printf("\nIndirizzo n.%d:",i); 
		printf("\nScrivi il tuo nome: "); 
		gets(AmiciPresenti[i].abitazione.nome);      /* struttura nidificata */ 
		printf("Scrivi la via: "); 
		gets(AmiciPresenti[i].abitazione.via);      /* idem */ 
		printf("Citta': "); 
		gets(AmiciPresenti[i].abitazione.citta);      /* idem */ 
		printf("Il tuo piatto preferito: "); 
		gets(AmiciPresenti[i].PiattoPreferito);      /* array non nidificato */ 
	}   

	/* Il quarto indirizzo lo ottengo copiandolo dal primo: */   
   	AmiciPresenti[3] = AmiciPresenti[0]; /* Copia di strutture */   

	/* Stampiamo qualche dato qua e là per verifica */   
   	printf("\n\nNome primo indirizzo: %s", AmiciPresenti[0].abitazione.nome); 
   	printf("\nVia secondo indirizzo: %s", AmiciPresenti[1].abitazione.via); 
   	printf("\nCittà terzo indirizzo: %s", AmiciPresenti[2].abitazione.citta); 
   	printf("\nFrase famosa primo indirizzo: %s", AmiciPresenti[0].PiattoPreferito); 
}  

È evidente come, questo sistema a strutture, possa essere molto vantaggioso, sia per la leggibilità del listato che per l'economia generale della gestione delle risorse assegnate al programma, in tutti i casi in cui si devono gestire grosse quantità di dati composti.

Ulteriori vantaggi, come vedremo nelle prossime lezioni, si potranno trarre associando alle strutture i puntatori.

Le unioni

Il concetto di union (unione) deriva direttamente da quello di struttura, ma con una importante differenza: i campi di una union rappresentano diversi modi di vedere o, per meglio dire di rappresentare, l'entità che la union stessa descrive.

In altri termini mentre, come già sappiamo, i campi di una struct descrivono, all’interno della stessa struttura, delle informazioni differenti e, a ciascun campo, è assegnata dal compilatore una determinata quantità di memoria, di modo che l’allocazione di memoria dell’intera struttura è pari alla somma di quella allocata per ciascuno dei suoi campi, per la union le cose vanno diversamente: per l’intera union viene allocata una quantità di memoria condiviso dalle sue variabili membro e di dimensione pari al più "grande" dei suoi membri. Questi, praticamente vengono "sovrapposti", in quanto condividono la stessa memoria fisica.

Una conseguenza di questo fatto è che inizializzando un campo della union vengono inizializzati anche tutti gli altri campi.

Anche le union trovano un "buon terreno" di applicazione in relazione ai puntatori.

Gli enumeratori

Un ulteriore strumento che il C rende disponibile per rappresentare più agevolmente dei gruppi di dati sono i tipi enum (enumeratori) altresì detti costanti enumerative. Questi consentono di descrivere con nomi simbolici gruppi di oggetti ai quali è possibile associare valori numerici interi. 
Vediamo un esempio:

enum giorni { 
	lunedì, 
	martedì, 
	mercoledì, 
	giovedì, 
	venerdì, 
	sabato, 
	domenica 
};   

Come si può vedere la dichiarazione di un enumeratore ricorda molto da vicino quella di una struttura: anche in questo caso viene definito un modello; la parola chiave questa volta è enum, seguita anche in questo caso dal nome che si intende dare al modello di enumeratore; vi sono le parentesi graffe aperta e chiusa, quest'ultima seguita dal punto e virgola.

La differenza più evidente rispetto alla dichiarazione di una struttura consiste nel fatto che laddove in questo compaiono le dichiarazioni dei campi (vere e proprie definizioni di variabili con tanto di indicatore di tipo e punto e virgola), nella dichiarazione di enum vi è un elenco dei nomi simbolici corrispondenti alle possibili manifestazioni della qualità che l'enumeratore stesso rappresenta ma il cui valore rimane costante. 
Anche la dichiarazione, l’inizializzazione e l’uso di una variabile di tipo enum ricorda molto da vicino quella di una variabile struttura:

	enum giorni oggi; 

	... 

	oggi = venerdì; 

	... 

	if(oggi == domenica) 
		printf("FESTIVO"); 
    else if(oggi == sabato) 
		printf("PREFESTIVO"); 
	else 
		printf("FERIALE");   

Da notare che il compilatore, di default, assegna anche dei valori ai nomi simbolici elencati nel modello dell'enum: al primo nome è associato il valore 0, al secondo 1, e così via.

E' comunque possibile assegnare valori a piacere, purché interi, ad uno o più nomi simbolici; ai restanti il valore viene assegnato automaticamente dal compilatore, incrementando di uno il valore associato al nome precedente.

enum giorni { 
	lunedì =1, 
	martedì, 
	mercoledì, 
	giovedì, 
	venerdì, 
	sabato, 
	domenica 
};   

Nell'esempio, al nome lunedì è assegnato esplicitamente valore 1: il compilatore assegna valore 2 al nome martedì e così via. I valori esplicitamente assegnati dal programmatore non devono necessariamente essere consecutivi; la sola condizione da rispettare è che si tratti di valori interi. 
Il vantaggio dell'uso degli enumeratori consiste nella semplicità di stesura e nella migliore leggibilità del programma oltre che nella limitazione dei possibili effetti collaterali legati all’utilizzo di variabili e costanti esplicite.


Bibliografia


Testi per l'approfondimento

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da Luca Sabatucci